package ext.plugin.flutter_websocket

import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.ws
import io.ktor.client.plugins.websocket.wss
import io.ktor.util.toMap
import io.ktor.websocket.close
import io.ktor.websocket.readBytes
import io.ktor.websocket.send
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

/** FlutterWebsocketPlugin */
class FlutterWebsocketPlugin : FlutterPlugin, MethodCallHandler {
    private lateinit var coroutineScope: CoroutineScope
    private lateinit var messenger: BinaryMessenger
    private lateinit var channel: MethodChannel
    private lateinit var client: HttpClient
    private val websocketSessions = mutableMapOf<String, WebSocketSessionApi>()

    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
        messenger = flutterPluginBinding.binaryMessenger
        channel =
            MethodChannel(flutterPluginBinding.binaryMessenger, "ext.plugin/flutter_websocket")
        channel.setMethodCallHandler(this)
        client = HttpClient(OkHttp) { install(WebSockets) }
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
        closeAllSessions()
        client.close()
        coroutineScope.cancel()
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "create" -> {

                val id: String = call.argument<String>("id")!!
                val url: String = call.argument<String>("url")!!
                val session = WebSocketSessionApi(id, messenger, client, url)
                websocketSessions[id] = session
                result.success(id)
            }

            "connect" -> {
                val id = call.arguments as String
                val session = websocketSessions[id]
                coroutineScope.launch(CoroutineExceptionHandler { _, throwable ->
                    result.error("connect-error", "connect url: ${session?.url} error", throwable)
                }) {
                    session?.connect() {
                        result.success(null)
                    }
                }
            }

            "send" -> {
                val id = call.argument<String>("id")!!
                val data = call.argument<ByteArray>("data")!!
                coroutineScope.launch(CoroutineExceptionHandler { _, throwable ->
                    result.error("send-error", "send data error", throwable)
                }) {
                    websocketSessions[id]?.send(data)
                    result.success(null)
                }
            }

            "sendText" -> {
                val id = call.argument<String>("id")!!
                val text = call.argument<String>("text")!!
                coroutineScope.launch(CoroutineExceptionHandler { _, throwable ->
                    result.error("send-error", "send text error", throwable)
                }) {
                    websocketSessions[id]?.send(text)
                    result.success(null)
                }
            }

            "close" -> {
                val id: String = call.arguments as String
                coroutineScope.launch(CoroutineExceptionHandler { _, throwable ->
                    result.error("close-error", "close session error", throwable)
                }) {
                    websocketSessions.remove(id)?.close()
                    result.success(null)
                }
            }

            else -> {
                result.notImplemented()
            }
        }
    }

    private fun closeAllSessions() {
        coroutineScope.launch {
            websocketSessions.forEach { entry ->
                entry.value.close()
            }
            websocketSessions.clear()
        }
    }
}

private class WebSocketSessionApi(
    id: String,
    messenger: BinaryMessenger,
    private val client: HttpClient,
    val url: String,
) : EventChannel.StreamHandler {
    private val eventSink = QueuingEventSink()
    private val event = EventChannel(messenger, "ext.plugin/websocketEvents$id")
    private var webSocketSession: DefaultClientWebSocketSession? = null

    init {
        event.setStreamHandler(this)
    }

    suspend fun connect(block: suspend DefaultClientWebSocketSession.() -> Unit) {
        eventSink.success(
            mapOf(
                "event" to "onState",
                "data" to 0
            )
        )
        connectInternal(client, url) {
            webSocketSession = this
            Log.i("Test", " connected: ${this.call.response.headers.toMap()}")
            block()
            eventSink.success(
                mapOf(
                    "event" to "onState",
                    "data" to 1
                )
            )
            try {
                for (frame in incoming) {
                    eventSink.success(
                        mapOf(
                            "event" to "onIncoming",
                            "data" to frame.readBytes()
                        )
                    )
                }
            } catch (e: Exception) {
                Log.e("Test", "incoming error", e)
                eventSink.error("ex_incoming", "read incoming data error", e)
            }
        }
    }

    suspend fun send(content: ByteArray) {
        webSocketSession?.send(content)
    }

    suspend fun send(content: String) {
        webSocketSession?.send(content)
    }

    suspend fun close() {
        eventSink.success(
            mapOf(
                "event" to "onState",
                "data" to 2
            )
        )
        webSocketSession?.close()
        eventSink.success(
            mapOf(
                "event" to "onState",
                "data" to 3
            )
        )
    }

    companion object {
        private suspend fun connectInternal(
            client: HttpClient,
            url: String, block:
            suspend DefaultClientWebSocketSession.() -> Unit
        ) {
            if (url.startsWith("ws")) {
                client.ws(urlString = url, block = block)
            } else if (url.startsWith("wss")) {
                client.wss(urlString = url, block = block)
            }
        }
    }

    override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
        eventSink.setDelegate(sink)
    }

    override fun onCancel(arguments: Any?) {
        eventSink.setDelegate(null)
    }
}
